Webhooks configuration
Our recommendation
We strongly recommend using webhooks to stay updated on important events. By subscribing to our webhook events, you can receive real-time notifications directly to your application, enabling you to act swiftly and efficiently.
Setting Up Webhooks
To start using webhooks, follow these steps:
Facilities Webhooks
Register Your Webhook: Use the Create Facilities Webhook endpoint to create webhooks.
Request Parameters:
Parameter Type Description url required
string The URL endpoint where webhook notifications will be sent events required
string An array of event to subscribe to. Refer to the list of Webhook Events for available event types facilities array An array of facility IDs to limit notifications to specific facilities secrets array An array of secret tokens for securing webhook payloads, a maximum of two secrets are allowed, If you do not provide a secret, a random secret will be generated for you Verify Webhook Signatures: For security, verify the signature of incoming webhook payloads to ensure they are sent from our system.
Handle Events: Implement logic in your application to process the received events and take appropriate actions.
User Webhooks
Register Your Webhook: Use the Create User Webhook endpoint to create webhooks.
Request Parameters:
Parameter Type Description url required
string The URL endpoint where webhook notifications will be sent events required
string An array of event to subscribe to. Refer to the list of Webhook Events for available event types secrets array An array of secret tokens for securing webhook payloads, a maximum of two secrets are allowed, If you do not provide a secret, a random secret will be generated for you Verify Webhook Signatures: For security, verify the signature of incoming webhook payloads to ensure they are sent from our system.
Handle Events: Implement logic in your application to process the received events and take appropriate actions.
How to verify the webhook signature:
The header of the request will look like this
```
Nursa-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v1=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
```
Steps to validate the webhook event signature
Get body payload from the incoming event request:
Get the Nursa-Signature header from the incoming event request
Split the header using the
,
character as the separator to get a list of attributes. Then split each attribute using the=
character as the separator to get a prefix and value pair. The value for the prefixt
corresponds to the timestamp in seconds, andv1
corresponds to the signature (or signatures if you have more than one secret).Concatenate the timestamp from the header, the character
.
and the JSON payload from the body.Sign the payload with the webhook secret using HMAC with SHA256 hash function, and encode the result with hex.
Compare the signatures
a. Compare the header's signature (or signatures) to the generated hash. If at least one of them matches, then the signature is correct (that's to guarantee no-downtime rotation).
b. Calculate the difference between the current and received timestamps, then decide if the difference is within your tolerance.
Code example on how to validate the request
- Python
- JavaScript
- Java
import hmac
import hashlib
from time import time
def main():
# Get request body in JSON format
body = "{\"data\":{\"shiftId\":\"c1099745-10d6-43ec-8c9d-6653d35ffbce\",\"facilityId\":\"f817ca7b-b2bb-4905-a74d-bc2ab403ffa3\",\"clinicianId\":\"1f559150-35a2-452f-98ec-7b9fe27dafbd\",\"at\":\"2023-06-19T20:51:38.372Z\",\"requestedBy\":{\"userId\":\"fb881bf1-f074-4f2c-aa09-422c7a360d2f\",\"email\":\"request@email.com\",\"source\":\"clinician\"}},\"eventType\":\"shift.request.created\"}"
# Get this from request header
signature = "t=1687208610,v1=29421185bad346abe4cbc1ee2048901addd3f9c0a3cff0d4d0022e91dbbdf8d5,v1=6004febfa2e2c5cf3f39e18ff3508ec49c99cad974d9678b6bf1b1a251bb6ca2"
# Must be stored in a safe location
secret = "df5c86cfe88295651cd8adb4e867084bfb08e3f522f4f2b967452871fa1a052a";
validate(body, signature, secret)
def validate(body, signature, secret):
attributes = signature.split(",")
timestamp = resolve_timestamp(attributes)
# 1687208610
payload_to_hash = timestamp + "." + body
hashed = hash_payload(payload_to_hash, secret)
# 29421185bad346abe4cbc1ee2048901addd3f9c0a3cff0d4d0022e91dbbdf8d5
is_valid_signature = len([attr for attr in attributes if attr == "v1=" + hashed]) == 1
is_valid_time = validate_time(timestamp)
print("Is signature valid? " + str(is_valid_signature) + ". Is valid time? " + str(is_valid_time));
return is_valid_signature and is_valid_time
def resolve_timestamp(attributes):
[key] = [attr for attr in attributes if attr.startswith("t=")]
return key.split("=")[1]
def hash_payload(payload, secret):
return hmac.new(bytes(secret, "utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()
def validate_time(timestamp):
now = int(time())
# You can define how much time you want to consider to prevent replay attacks
five_minutes_in_seconds = 5 * 60
return now < int(timestamp) + five_minutes_in_seconds
main()
const { createHmac } = require('crypto');
function main() {
/* Get request body in JSON string format */
const body =
'{"data":{"shiftId":"c1099745-10d6-43ec-8c9d-6653d35ffbce","facilityId":"f817ca7b-b2bb-4905-a74d-bc2ab403ffa3","clinicianId":"1f559150-35a2-452f-98ec-7b9fe27dafbd","at":"2023-06-19T20:51:38.372Z","requestedBy":{"userId":"fb881bf1-f074-4f2c-aa09-422c7a360d2f","email":"request@email.com","source":"clinician"}},"eventType":"shift.request.created"}';
/* Get this from request header "nursa-signature" */
const signature =
't=1687208610,v1=29421185bad346abe4cbc1ee2048901addd3f9c0a3cff0d4d0022e91dbbdf8d5,v1=6004febfa2e2c5cf3f39e18ff3508ec49c99cad974d9678b6bf1b1a251bb6ca2';
/** Must be stored in a safe location */
const secret = 'df5c86cfe88295651cd8adb4e867084bfb08e3f522f4f2b967452871fa1a052a';
validate(body, signature, secret);
}
function validate(body, signature, secret) {
const signatureAttributes = signature.split(',').map((attribute) => {
const [key, value] = attribute.split('=');
return { key, value };
});
const timestamp = signatureAttributes.find((attr) => attr.key === 't').value;
// 1687208610
const isValidTime = validateExpiration(Number(timestamp));
const payloadToHash = `${timestamp}.${body}`;
const hashed = hashPayload(payloadToHash, secret);
// 29421185bad346abe4cbc1ee2048901addd3f9c0a3cff0d4d0022e91dbbdf8d5
const isValidSignature = !!signatureAttributes.find(
(attr) => attr.key === 'v1' && attr.value === hashed
);
console.log(`Is signature valid? ${isValidSignature}. Is valid time? ${isValidTime}`);
return isValidTime && isValidSignature;
}
function hashPayload(payload, secret) {
return createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
}
function validateExpiration(timestamp) {
const currentTime = Math.floor(Date.now() / 1000);
// You can define how much time you want to consider to prevent replay attacks
const fiveMinutesInSeconds = 5 * 60;
return currentTime < timestamp + fiveMinutesInSeconds;
}
main();
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Mac;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.nio.charset.StandardCharsets;
import java.lang.StringBuilder;
import java.util.Arrays;
import java.util.List;
import java.util.Date;
public class NursaSignatureValidator {
public static void main(String args[]) {
/* Get request body in JSON format */
String body = "{\"data\":{\"shiftId\":\"c1099745-10d6-43ec-8c9d-6653d35ffbce\",\"facilityId\":\"f817ca7b-b2bb-4905-a74d-bc2ab403ffa3\",\"clinicianId\":\"1f559150-35a2-452f-98ec-7b9fe27dafbd\",\"at\":\"2023-06-19T20:51:38.372Z\",\"requestedBy\":{\"userId\":\"fb881bf1-f074-4f2c-aa09-422c7a360d2f\",\"email\":\"request@email.com\",\"source\":\"clinician\"}},\"eventType\":\"shift.request.created\"}";
/* Get this from request header */
String signature = "t=1687208610,v1=29421185bad346abe4cbc1ee2048901addd3f9c0a3cff0d4d0022e91dbbdf8d5,v1=6004febfa2e2c5cf3f39e18ff3508ec49c99cad974d9678b6bf1b1a251bb6ca2";
/** Must be stored in a safe location */
String secret = "df5c86cfe88295651cd8adb4e867084bfb08e3f522f4f2b967452871fa1a052a";
validate(body, signature, secret);
}
public static boolean validate(String body, String signature, String secret) {
List<String> attributes = Arrays.asList(signature.split(","));
String timestamp = resolveTimestamp(attributes);
// 1687208610
String payloadToHash = timestamp + "." + body;
String hashed = hashPayload(payloadToHash, secret);
// 29421185bad346abe4cbc1ee2048901addd3f9c0a3cff0d4d0022e91dbbdf8d5
boolean isValidSignature = attributes.contains("v1=" + hashed);
boolean isValidTime = validateTime(timestamp);
System.out.println("Is signature valid? " + isValidSignature + ". Is valid time? " + isValidTime);
return isValidSignature && isValidTime;
}
private static String hashPayload(String payload, String secret) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKeySpec);
return getHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
return null;
} catch (InvalidKeyException e) {
return null;
}
}
private static boolean validateTime(String timestamp) {
try{
long time = Long.parseLong(timestamp);
// You can define how much time you want to consider to prevent replay attacks
long fiveMinutesInSeconds = 5 * 60;
long now = new Date().getTime() / 1000;
return now < time + fiveMinutesInSeconds;
} catch (NumberFormatException ex){
return false;
}
}
private static String resolveTimestamp(List<String> attributes) {
String timeAttribute = attributes.stream()
.filter(attribute -> attribute.startsWith("t="))
.findAny()
.orElse(null);
return timeAttribute.split("=")[1];
}
private static String getHex( byte [] raw ) {
StringBuilder hexString = new StringBuilder();
for (byte b : raw) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
Why this helps?
Webhooks provide a mechanism for your system to receive notifications when specific events occur in our system. By utilizing webhooks, you can:
- Receive instant updates: Get notified immediately when an event happens.
- Reduce API polling: Minimize the need to frequently check our API for changes.
- Enhance efficiency: Automate workflows and processes in response to real-time events.
Facilities Webhook Events
Event | Description |
---|---|
shift.created | Triggered when a shift is created. |
shift.request.created | Triggered when a shift request is created. |
shift.request.cancelled | Triggered when a shift request is cancelled. |
shift.report.created | Triggered when a shift report is created. |
shift.scheduled.cancelled | Triggered when a scheduled shift is cancelled. |
shift.scheduled | Triggered when a shift is scheduled. |
shift.report.accepted-automatically | Triggered when a shift report is automatically accepted. |
shift.report.accepted | Triggered when a shift report is accepted. |
shift.report.rejected | Triggered when a shift report is rejected. |
shift.cancelled | Triggered when a shift is cancelled. |
all | Triggers notifications for all Facilities events. |
User Webhook Events
| facility.user-connection.accepted
| Triggered when a facility user connection is accepted. |
| facility.user-connection.rejected
| Triggered when a facility user connection is rejected. |
| facility.creation.accepted
| Triggered when a facility creation request is accepted. |
| facility.creation.rejected
| Triggered when a facility creation request is rejected. |
| all
| Triggers notifications for all User events. |
Please refer to the Webhooks Events page to check the payload of each event.